JavaScript 学习笔记:对象的属性描述符

JavaScript 中的对象属性,并不只是简单的键值对,通过属性描述符 property descriptor ,我们可以更加灵活的配置对象属性以控制属性的行为,包括是否可枚举、是否可写、是否可配置,实现更强大的功能。实际上,对于对象而言,一共有 2 种属性:

  • 数据属性 data property
  • 访问属性 accessor property

数据属性

通常我们见到的属性都是这样的:

1
2
3
4
5
// 代码片段 1
const user = {
name: 'Sheldon',
age: 29
};

上面的 nameage 都是数据属性。

访问属性

还有另一种形式,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 代码片段 2
const user = {
name: 'Bill',
surname: 'Gates',
get fullname() {
return `${this.name} ${this.surname}`;
},
set fullname(value) {
[this.name, this.surname] = value.split(',');
}
};

user.fullname // Bill Gates
user.fullname = 'Sheldon Cooper';
user.name // Sheldon

上面的 namesurname 是数据属性,fullname 则是访问属性。即凡是使用 get prop() 或者 set prop() 定义的属性,都是访问属性,不再是数据属性。

数据属性的描述符

对于数据属性而言,其描述符由 4 个 flag 组成:

  • value 属性的值
  • writable 是否可写
  • enumerable 是否可枚举,在类似 for...in 遍历中是否忽略
  • configurable 是否可配置,即是否可以编辑描述符的 flag

对于使用字面量声明的对象属性而言,除了 value 之外的 3 个 flag 的默认值都为 true。使用 Object.getOwnPropertyDescriptor(obj, prop) 方法可以获取对象上某属性的描述符。比如获取上面代码片段 1 中 user 对象 name 属性的描述符:

1
2
3
4
5
6
7
8
// 代码片段 3
const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
// {
// "value": "Sheldon",
// "writable": true,
// "enumerable": true,
// "configurable": true
// }

如果想要精确地控制属性的行为,使用 Object.defineProperty(obj, prop, descriptor) 方法。使用这种方式,如果属性的某个 flag 没有显式指定,则默认值将是 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 代码片段 4
const user = {};
Object.defineProperty(user, 'name', {
value: 'Sheldon',
writable: true
})

const descriptor = Object.getOwnPropertyDescriptor(user, 'name');
console.log(JSON.stringify(descriptor, null, 2));
// {
// "value": "Sheldon",
// "writable": true,
// "enumerable": false,
// "configurable": false
// }

可以看到,除了显式指定的 writable ,其他未指定的 flag 的都是默认为 false

可枚举性

描述符的 enumerable 属性,称为“可枚举性”,如果该属性为 false,就表示某些操作会忽略当前属性。
目前,有四个操作会忽略 enumerablefalse 的属性。

  • for...in 循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略 enumerablefalse 的属性,只拷贝对象自身的可枚举的属性。

可配置性

描述符的 configurable 属性,表示该属性的 flag 是否可配置,一旦设为 false 不可逆。用的很少。

访问属性的描述符

访问属性的描述符不同于数据属性的描述符,没有 valuewritable 这 2 个 flag,多了 getset,所以一个访问属性的描述符可能有如下组成:

  • get – 无参数函数,当属性被读取时被调用
  • set – 单参数函数,当属性被设置时被调用
  • enumerable – 与数据属性一致
  • configurable – 与数据属性一致

访问属性的 getter/setter 可以直接使用字面量声明(如上面的代码片段 2 所示),也可以使用 Object.defineProperty,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 代码片段 5
const user = {
name: 'Bill',
surname: 'Gates'
};

Object.defineProperty(user, 'fullname', {
get() {
return `${this.name} ${this.surname}`;
},
set(value) {
[this.name, this.surname] = value.split(',');
}
})

getter/setter 应用

使用 getset 访问属性描述符,我们可以更加灵活控制属性的读写行为。比如对于 user 对象而言,可以限制 name 的长度:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 代码片段 6
const user = {
get name() {
return this._name;
},
set name(value) {
if (value.length > 4) {
alert('用户名长度不能超过 4 位');
return;
}
this._name = value; // 这里使用 this._name 是一种广泛的通用约定
}
}

数据属性或访问属性

需要注意的是,一个属性要么是数据属性,要么是访问属性,不能两者皆是。相应地,如果同时给属性设置 valueget 这 2 个 flag 会报错。

1
2
3
4
5
6
7
8
9
// 代码片段 7
// Error: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute
Object.defineProperty({}, 'prop', {
get() {
return 1
},

value: 1
});

参考链接